feat(db): persist users + resource selection — Part of #586 #980
feat(db): persist users + resource selection — Part of #586 #980skypank-coder wants to merge 6 commits into
Conversation
…WASP#586) Persist accounts on OIDC login and store a per-user resource (standard) selection. Additive to the existing session auth: the callback upserts the User row and sets session['user_id'] only when CRE_ENABLE_LOGIN is on; the existing session keys and 401 behaviour are unchanged. - User(google_sub UNIQUE, email, display_name, created_at, last_seen_at) - UserResourceSelection(user_id FK CASCADE, standard_name, UNIQUE(user_id, name)) - Node_collection: upsert_user / get_user_by_sub / get_user_resource_selection / set_user_resource_selection - Alembic: dedicated merge of the two open heads + create tables (up/down verified on Postgres; guardrail green)
…int (OWASP#586) Address PR review: - upsert_user now mirrors add_node: catch IntegrityError, rollback, re-query by google_sub, update profile — safe under concurrent logins. - Drop column-level unique=True on User.google_sub; keep only the named constraint so create_all matches the Alembic migration (no schema drift). - Login callback skips persistence when the OIDC 'sub' claim is missing. - Callback test asserts session['user_id'] equals the persisted User.id.
…OWASP#586) Address PR review: - Log only the exception class on login-persistence failure; the raw message can carry SQL parameters (email, OIDC sub). - Add callback test: login enabled but token missing 'sub' creates no user row and leaves session['user_id'] unset.
Summary by CodeRabbit
WalkthroughChangesUser persistence
Estimated code review effort: 3 (Moderate) | ~30 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
application/database/db.py (2)
1038-1089: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHoist the repeated
datetime/timezoneimport to module scope.
from datetime import datetime, timezoneis imported locally in bothupsert_user(Line 1046) andset_user_resource_selection(Line 1105). Move it to the top-level imports once.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@application/database/db.py` around lines 1038 - 1089, Move the repeated “from datetime import datetime, timezone” import from the methods upsert_user and set_user_resource_selection to the module-level imports, leaving both methods to use the shared top-level import without local import statements.
1101-1125: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
set_user_resource_selectionhas no guard against concurrent duplicate inserts.Unlike
upsert_userabove (which recovers fromIntegrityErrorongoogle_subraces), this delete-then-insert sequence has no equivalent safeguard. Two concurrent calls for the sameuser_id(e.g. a double-submitted "save selection" request) could both pass the delete step and then collide onuq_user_resource_selectionfor a sharedstandard_name, raising an unhandledIntegrityError. No endpoint calls this yet, but it's worth hardening before wiring it up.♻️ Suggested guard mirroring upsert_user's recovery pattern
self.session.query(UserResourceSelection).filter( UserResourceSelection.user_id == user_id ).delete() for name in deduped: self.session.add( UserResourceSelection( id=generate_uuid(), user_id=user_id, standard_name=name, created_at=now, ) ) - self.session.commit() + try: + self.session.commit() + except IntegrityError: + # A concurrent call raced on the same (user_id, standard_name) pair; + # the other writer's result wins, just return the current state. + self.session.rollback() return self.get_user_resource_selection(user_id)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@application/database/db.py` around lines 1101 - 1125, Harden set_user_resource_selection against concurrent IntegrityError races during its delete-and-insert sequence. Mirror the existing recovery pattern used by upsert_user: catch the uniqueness violation, roll back the failed transaction, and return the persisted selection for user_id without leaving the exception unhandled; preserve the current deduplication and replacement behavior for non-conflicting calls.application/web/web_main.py (1)
1164-1185: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueBroad exception catch is intentional but worth narrowing eventually.
Ruff flags the blind
except Exceptionhere (BLE001). The rationale in the comment (avoid leaking SQL parameters) is sound forupsert_userfailures specifically, but the broad catch also silently swallows unrelated bugs (e.g., a malformedid_infodict) that would otherwise be visible. Consider catchingSQLAlchemyErrorspecifically if you want DB failures to degrade gracefully while other exceptions still surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@application/web/web_main.py` around lines 1164 - 1185, In the user persistence block around upsert_user, replace the broad Exception handler with SQLAlchemyError and import that exception from the project’s SQLAlchemy dependency. Preserve the existing sanitized class-name logging and graceful redirect for database failures, while allowing unrelated errors such as malformed id_info data to propagate.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@application/database/db.py`:
- Around line 1038-1089: Move the repeated “from datetime import datetime,
timezone” import from the methods upsert_user and set_user_resource_selection to
the module-level imports, leaving both methods to use the shared top-level
import without local import statements.
- Around line 1101-1125: Harden set_user_resource_selection against concurrent
IntegrityError races during its delete-and-insert sequence. Mirror the existing
recovery pattern used by upsert_user: catch the uniqueness violation, roll back
the failed transaction, and return the persisted selection for user_id without
leaving the exception unhandled; preserve the current deduplication and
replacement behavior for non-conflicting calls.
In `@application/web/web_main.py`:
- Around line 1164-1185: In the user persistence block around upsert_user,
replace the broad Exception handler with SQLAlchemyError and import that
exception from the project’s SQLAlchemy dependency. Preserve the existing
sanitized class-name logging and graceful redirect for database failures, while
allowing unrelated errors such as malformed id_info data to propagate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 4fb83f8c-bd64-4f81-84cb-24a695e2c0d6
📒 Files selected for processing (6)
application/database/db.pyapplication/tests/user_model_test.pyapplication/tests/web_main_test.pyapplication/web/web_main.pymigrations/versions/a1b2c3d4e5f6_add_users_and_resource_selection.pymigrations/versions/f0e1d2c3b4a5_merge_heads_before_users.py
What & why
First increment of #586 ("Add support for users"): login-first user
persistence. OIDC login existed but nothing was persisted — no User table,
nowhere to hang per-user data. This adds that foundation so per-user resource
filtering (PR2–PR4) can be built on it. Aligns with auth RFC #876 (TODO 1/2).
Changes
userstable anchored on the immutable OIDCsub(google_sub); emailand display_name refreshed on each login.
user_resource_selectiontable: one row per selected standard,UNIQUE(user_id, standard_name),FK → users.id ON DELETE CASCADE.session['user_id']— gated onCRE_ENABLE_LOGIN, wrapped so it never blocks login. Existing session keysand the anonymous 401 are unchanged (intentionally narrower than RFC RFC: User Authentication and Online MyOpenCRE Mapping #876's
login_requiredrewrite).Node_collectionmethods:upsert_user,get_user_by_sub,get_user_resource_selection,set_user_resource_selection. These back the/rest/v1/userresource endpoints coming in PR2.open heads (
ab12cd34ef56,9b1c2d3e4f50), then a revision creates the twotables — keeping the head single-parent so
flask db downgradeisunambiguous.
Testing
dedup / per-user isolation, unique constraint, cascade delete, callback upsert,
and the flag-off no-op path.
ON DELETE CASCADEenforced at the DB level (rows 1 → 0), both uniqueconstraints created cleanly, migration
upgrade → downgrade → upgradedeterministic, alembic revision guardrail green. 12/12 pass on Postgres.
blackclean; no new mypy errors vs. baseline.Scope boundaries
No new endpoints, no server-side filtering, no frontend, no
login_requiredrewrite — those are the follow-up PRs in #586.
Unrelated pre-existing issue (out of scope, flagging for visibility)
flask db upgradefrom a blank DB fails on Postgres before reaching myrevisions: migrations
3c65127871a6/7bf4eac76958reuse the constraint nameuq_pair(authored via SQLitebatch_alter_table), which collides on afrom-base replay. Postgres rolls the whole thing back cleanly (transactional
DDL). This predates this branch and doesn't affect the prod path (apply-onto-
existing-schema), so it's out of scope here — but worth a separate fix, since
anyone bootstrapping a fresh Postgres DB will hit it.